import enum
from datetime import datetime

from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.db import DatabaseError, IntegrityError, models, transaction
from django.utils import timezone

from sysreptor.pentests import cvss
from sysreptor.users.models import PentestUser
from sysreptor.utils import license
from sysreptor.utils.configuration import configuration
from sysreptor.utils.language import Language
from sysreptor.utils.models import BaseModel
from sysreptor.utils.relations import GenericOneToOneForeignKey, GenericOneToOneRelation


class LockStatus(enum.Enum):
    CREATED = 'created'
    REFRESHED = 'refreshed'
    FAILED = 'failed'


class LockInfo(BaseModel):
    # Generic foreign key to arbitrary/multiple models
    content_type = models.ForeignKey(to=ContentType, on_delete=models.CASCADE)
    object_id = models.UUIDField()
    locked_object = GenericOneToOneForeignKey(ct_field='content_type', fk_field='object_id')

    last_ping = models.DateTimeField(default=timezone.now)
    user = models.ForeignKey(to=PentestUser, on_delete=models.CASCADE, null=False)

    class Meta:
        unique_together = [('content_type', 'object_id')]

    @property
    def expires(self) -> datetime:
        return (self.last_ping or timezone.now()) + settings.MAX_LOCK_TIME

    def refresh_lock(self):
        try:
            self.last_ping = timezone.now()
            self.save(force_update=True)
            return LockStatus.REFRESHED
        except (self.DoesNotExist, DatabaseError):
            return LockStatus.FAILED


class LockableMixin(models.Model):
    lock_info_data = GenericOneToOneRelation(to=LockInfo)

    class Meta:
        abstract = True

    @property
    def lock_info(self):
        if not self.is_locked:
            return None
        return self.lock_info_data

    @property
    def is_locked(self):
        return self.lock_info_data is not None and timezone.now() <= self.lock_info_data.expires

    def _cleanup_old_lock(self):
        if self.lock_info_data:
            self.lock_info_data.delete()
            self.lock_info_data = None

    def lock(self, user, refresh_lock=True):
        if self.is_locked and self.lock_info.user != user:
            # Already locked by another user
            return LockStatus.FAILED
        elif self.is_locked and self.lock_info.user == user:
            # Refresh lock such that it does not expire
            if refresh_lock:
                return self.lock_info.refresh_lock()
            else:
                return LockStatus.REFRESHED
        elif not self.is_locked:
            with transaction.atomic():
                self._cleanup_old_lock()
                try:
                    self.lock_info_data = LockInfo.objects.create(locked_object=self, user=user)
                    return LockStatus.CREATED
                except IntegrityError:
                    self.lock_info_data = LockInfo.objects.get(content_type=ContentType.objects.get_for_model(self), object_id=self.id)
                    if self.lock_info_data.user == user:
                        return LockStatus.REFRESHED
                    else:
                        return LockStatus.FAILED
        return LockStatus.FAILED

    def unlock(self, user):
        if not self.is_locked:
            self._cleanup_old_lock()
            return True
        elif self.is_locked and self.lock_info.user == user:
            self._cleanup_old_lock()
            return True
        else:
            return False


class SourceEnum(models.TextChoices):
    CREATED = 'created', 'Created'
    IMPORTED = 'imported', 'Imported'
    IMPORTED_DEPENDENCY = 'imported_dependency', 'Imported Dependency'
    CUSTOMIZED = 'customized', 'Customized'
    SNAPSHOT = 'snapshot', 'Snapshot'


class ImportableMixin(models.Model):
    source = models.CharField(max_length=50, choices=SourceEnum.choices, default=SourceEnum.CREATED, db_index=True, editable=False)

    class Meta:
        abstract = True


class ReviewStatus(enum.StrEnum):
    IN_PROGRESS = 'in-progress'
    READY_FOR_REVIEW = 'ready-for-review'
    NEEDS_IMPROVEMENT = 'needs-improvement'
    FINISHED = 'finished'
    DEPRECATED = 'deprecated'

    @classmethod
    def get_definitions(cls, include_deprecated: bool = False) -> list[dict]:
        static_status_dict = {s['id']: s for s in [
            {'id': ReviewStatus.IN_PROGRESS, 'label': 'In Progress', 'icon': 'mdi-pencil'},
            {'id': ReviewStatus.FINISHED, 'label': 'Finished', 'icon': 'mdi-check-all'},
            {'id': ReviewStatus.DEPRECATED, 'label': 'Deprecated', 'icon': 'mdi-close'},
        ]}
        custom_statuses = configuration.STATUS_DEFINITIONS if license.is_professional() else \
              configuration.definition['STATUS_DEFINITIONS'].default
        custom_status_dict = {s['id']: s for s in custom_statuses}

        all_statuses = [
            static_status_dict[ReviewStatus.IN_PROGRESS] | custom_status_dict.get(ReviewStatus.IN_PROGRESS, {}),
            *[s for s in custom_statuses if s['id'] not in static_status_dict],
            static_status_dict[ReviewStatus.FINISHED] | (custom_status_dict.get(ReviewStatus.FINISHED, {})),
            *([static_status_dict[ReviewStatus.DEPRECATED] | (custom_status_dict.get(ReviewStatus.DEPRECATED, {}))]
              if (include_deprecated or ReviewStatus.DEPRECATED in custom_status_dict) else []),
        ]
        for status in all_statuses:
            if not status.get('allowed_next_statuses'):
                status['allowed_next_statuses'] = None
        return all_statuses

    @classmethod
    def get_status_display(cls, value: str) -> str:
        return next(iter([d['label'] for d in cls.get_definitions(include_deprecated=True) if d['id'] == value]), None) or value

    @classmethod
    def validate_transition(cls, current_status: str | None, new_status: str, raise_exception = False) -> None:
        """
        Validate if transitioning from current_status to new_status is allowed.
        Raises ValueError if the transition is not allowed.
        """
        if not current_status:
            # Creating new object, any status is allowed
            return True

        if current_status == new_status:
            # No change, always allowed
            return True

        all_definitions = cls.get_definitions(include_deprecated=True)
        current_status_def = next((s for s in all_definitions if s['id'] == current_status), None)
        if not current_status_def:
            # Unknown current status, allow any transition
            return True

        allowed_next = current_status_def.get('allowed_next_statuses')
        if not allowed_next:
            # No restrictions defined, allow any transition
            return True

        if new_status not in allowed_next:
            if raise_exception:
                raise ValidationError(
                    f'Cannot transition from status "{current_status}" to "{new_status}". '
                    f'Allowed statuses: {", ".join(allowed_next)}',
                )
            return False
        return True


def get_default_language():
    if preferred_languages := configuration.get('PREFERRED_LANGUAGES', skip_db_query=True):
        return Language(preferred_languages[0])
    return Language.ENGLISH_US


class LanguageMixin(models.Model):
    language = models.CharField(choices=Language.choices, default=get_default_language, max_length=5, db_index=True)

    class Meta:
        abstract = True


def get_risk_score_from_data(data: dict):
    cvss_vector = data.get('cvss')
    score = None
    level = None

    if isinstance(cvss_vector, dict):
        score = cvss_vector.get('score')
        level = cvss_vector.get('level')
        cvss_vector = cvss_vector.get('vector')
    elif cvss_vector:
        score = cvss.calculate_score(cvss_vector)
        level = cvss.level_from_score(score)
    if severity := data.get('severity'):
        level = severity

    return {
        'cvss_vector': cvss_vector,
        'score': score,
        'level': level,
    }
